﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Exceptions;
using RockBus.Logging.Interfaces;
using RockBus.Messages.Interfaces;
using RockBus.PerformanceCounters.Interfaces;
using RockBus.Routing.Interfaces;
using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.ServiceModel.Channels;
using System.Threading.Tasks;

namespace RockBus.MessageProcessors.SendPorts
{
    public class DeliverySendPort : IMessageProcessor
    {
        private const string OperationCounterName = "DeliverMessage";

        public DeliverySendPort(
            ILogger logger,
            IReadOnlyConfiguration configuration,
            IPerformanceCounterFactory performanceCounterFactory,
            IMessageFactory messageFactory,
            IPipelineManager pipelineManager,
            IQueueManager queueManager,
            ITransportManager transportManager
            )
        {
            this.Logger = logger;
            this.Configuration = configuration;
            this.PerformanceCounterFactory = performanceCounterFactory;
            this.MessageFactory = messageFactory;
            this.PipelineManager = pipelineManager;
            this.QueueManager = queueManager;
            this.TransportManager = transportManager;
        }

        private ILogger Logger { get; set; }

        private IReadOnlyConfiguration Configuration { get; set; }

        private IPerformanceCounterFactory PerformanceCounterFactory { get; set; }

        private IMessageFactory MessageFactory { get; set; }

        private IPipelineManager PipelineManager { get; set; }

        private IQueueManager QueueManager { get; set; }

        private ITransportManager TransportManager { get; set; }

        public MessageProcessorEnum ProcessingStep { get { return MessageProcessorEnum.DeliverySendPort; } }

        public void ProcessMessage(IMessageContext messageContext)
        {
            Contract.Requires(messageContext.RoutingHeaders.Count() == 1, "Incorrect number of RoutingHeaders specified.");

            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "{0}", messageContext);

            var oc = this.PerformanceCounterFactory.CreateOperationCounter(OperationCounterName);
            oc.Start();

            try
            {
                var rh = messageContext.RoutingHeaders.First();
                var endpoint = rh.DestinationEndpoint;
                if (endpoint == null)
                {
                    // No destination endpoint means ReplyEndpoint is empty,
                    //  so this message is finished.
                    messageContext.ProcessingData.MessageDirection = MessageDirectionEnum.End;
                    this.QueueManager.ProcessMessage(messageContext);
                    return;
                }

                if ((messageContext.ProcessingData.MessageDirection == MessageDirectionEnum.InboundRequest) ||
                    (messageContext.ProcessingData.MessageDirection == MessageDirectionEnum.InboundResponse) ||
                    (messageContext.ProcessingData.MessageDirection == MessageDirectionEnum.End))
                {
                    Debug.Fail("Unexpected MessageDirection");
                    throw new RoutingException("Unexpected MessageDirection of message being delivered by DeliverySendPort.");
                }

                var transport = this.TransportManager.GetOrCreateClient(endpoint);
                var message = this.MessageFactory.CreateServiceModelMessage(
                    messageContext,
                    rh,
                    transport.MessageVersion,
                    false,
                    endpoint.IsCorrelated);

                // throws System.InvalidOperationException: A TransactionScope must be disposed on the same thread that it was created.
                //using (var ts = messageContext.CreateTransactionScope(endpoint.TransactionScopeOption))
                //{
                //    var responseMessage = await transport.StartTransportMessage(message);
                //    // The MessageFactory sets the MessageDirection
                //    responseContext = this.MessageFactory.CreateResponseMessageContext(messageContext, responseMessage);
                //    ts.Complete();
                //}

                Task<Message> transportTask = null;
                using (var ts = messageContext.CreateTransactionScope(endpoint.TransactionScopeOption, this.Configuration.InternalConfiguration.IsolationLevel, this.Configuration.InternalConfiguration.TransactionTimeoutMs))
                {
                    message = this.PipelineManager.ProcessOutboundRequest(message, rh.SubscriptionDetail);

                    this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "SENDING to {0}: {1}", endpoint.Address, messageContext);
                    transportTask = transport.StartTransportMessage(message);
                    ts.Complete();
                }

                // Success
                transportTask.ContinueWith((task) =>
                {
                    try
                    {
                        if ((task.Result != null) &&
                            (task.Result.IsFault))
                        {
                            this.Logger.LogError(MethodBase.GetCurrentMethod(), "SENDING FAILED (fault) to {0}: {1}", endpoint.Address, messageContext);

                            var faultContext = this.MessageFactory.CreateFaultMessageContext(messageContext, task.Result);
                            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "FAULT: {0}", faultContext.ResponseMessage.MessageAsString());
                            this.QueueManager.ProcessException(faultContext);
                            return;
                        }

                        // The MessageFactory sets the MessageDirection
                        if (endpoint.ExchangeType == ExchangeTypeEnum.TwoWayRequestResponse)
                        {
                            if (task.Result == null)
                            {
                                this.Logger.LogError(MethodBase.GetCurrentMethod(), "SENDING FAILED (no result) to {0}: {1}", endpoint.Address, messageContext);

                                throw new DeliveryException("No response received on TwoWayRequestResponse Endpoint.");
                            }

                            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "SENDING SUCCEEDED to {0}: {1}", endpoint.Address, messageContext);

                            var responseMessage = this.PipelineManager.ProcessInboundResponse(task.Result, rh.SubscriptionDetail);

                            // Post the response back to the QueueManager
                            var responseContext = this.MessageFactory.CreateResponseMessageContext(messageContext, responseMessage);
                            this.QueueManager.ProcessMessage(responseContext);
                        }
                        else
                        {
                            // For any non-RR Endpoint ExchangeTypes, this message is finished
                            messageContext.ProcessingData.MessageDirection = MessageDirectionEnum.End;
                            this.QueueManager.ProcessMessage(messageContext);
                        }
                    }
                    catch (Exception exc)
                    {
                        messageContext.Exception = exc;
                        this.QueueManager.ProcessException(messageContext);
                    }
                    finally
                    {
                        oc.Complete();
                    }
                }, TaskContinuationOptions.OnlyOnRanToCompletion);

                // Failure
                transportTask.ContinueWith((task) =>
                {
                    try
                    {
                        this.Logger.LogError(MethodBase.GetCurrentMethod(), "SENDING FAILED to {0}: {1}; EXCEPTION: ", endpoint.Address, messageContext.ToString(), task.Exception.ToString());

                        messageContext.Exception = task.Exception;
                        this.QueueManager.ProcessException(messageContext);
                    }
                    catch
                    {
                        // What to do here?
                    }
                    finally
                    {
                        oc.Complete();
                    }
                }, TaskContinuationOptions.OnlyOnFaulted);
            }
            catch
            {
                oc.Abort();
                throw;
            }
            //finally
            //{
            //    oc.Complete();
            //}
        }
    }
}